package com.sz.netty.transport;

import java.math.BigDecimal;
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;

import lombok.extern.slf4j.Slf4j;

/**
 * Transport context for maintaining device state
 */
@Slf4j
public class TransportContext {
    private String deviceId;
    private TransportPoint startPoint;
    private boolean hasUnpairedStart;
    private Date lastUpdateTime;
    
    // Sliding windows for vibration and radar records
    private Queue<VibrationRecord> vibrationRecords = new LinkedList<>();
    private Queue<RadarRecord> radarRecords = new LinkedList<>();
    
    // Track GPS points between start and end for precise distance calculation
    private Queue<TransportPoint> gpsPoints = new LinkedList<>();
    
    // Getters and setters
    public String getDeviceId() {
        return deviceId;
    }
    
    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }
    
    public TransportPoint getStartPoint() {
        return startPoint;
    }
    
    public void setStartPoint(TransportPoint startPoint) {
        this.startPoint = startPoint;
    }
    
    public boolean isHasUnpairedStart() {
        return hasUnpairedStart;
    }
    
    public void setHasUnpairedStart(boolean hasUnpairedStart) {
        this.hasUnpairedStart = hasUnpairedStart;
    }
    
    public Date getLastUpdateTime() {
        return lastUpdateTime;
    }
    
    public void setLastUpdateTime(Date lastUpdateTime) {
        this.lastUpdateTime = lastUpdateTime;
    }
    
    /**
     * Add a vibration record to the sliding window
     */
    public void addVibrationRecord(VibrationRecord record) {
        vibrationRecords.offer(record);
    }
    
    /**
     * Add a radar record to the sliding window
     */
    public void addRadarRecord(RadarRecord record) {
        radarRecords.offer(record);
    }
    
    /**
     * Get vibration records
     */
    public Queue<VibrationRecord> getVibrationRecords() {
        return vibrationRecords;
    }
    
    /**
     * Get radar records
     */
    public Queue<RadarRecord> getRadarRecords() {
        return radarRecords;
    }
    
    /**
     * Add a GPS point for precise distance calculation
     */
    public void addGpsPoint(TransportPoint gpsPoint) {
        gpsPoints.offer(gpsPoint);
    }
    
    /**
     * Get GPS points
     */
    public Queue<TransportPoint> getGpsPoints() {
        return gpsPoints;
    }
    
    /**
     * Clear GPS points
     */
    public void clearGpsPoints() {
        gpsPoints.clear();
    }
    
    /**
     * Calculate precise transport distance by accumulating distances between consecutive GPS points
     * @return precise transport distance in meters
     */
    public double calculatePreciseTransportDistance(TransportPoint startPoint, TransportPoint endPoint) {
        log.debug("Calculating precise transport distance for device {}", deviceId);
        
        // Log detailed distance calculation data
        com.sz.netty.util.TransportEventLogger.logDistanceCalculationData(deviceId, startPoint, endPoint, gpsPoints);
        
        // Check if start or end points have invalid coordinates (null or zero)
        if (isInvalidCoordinate(startPoint.getLatitude(), startPoint.getLongitude()) || 
            isInvalidCoordinate(endPoint.getLatitude(), endPoint.getLongitude())) {
            log.warn("Start or end point has invalid coordinates (null or zero), skipping distance calculation");
            return 0.0;
        }
        
        // If no intermediate points, use direct distance
        if (gpsPoints.isEmpty()) {
            double directDistance = calculateHaversineDistance(
                startPoint.getLatitude(), startPoint.getLongitude(),
                endPoint.getLatitude(), endPoint.getLongitude());
            log.debug("No intermediate GPS points, using direct distance: {}m", directDistance);
            return directDistance;
        }
        
        // Start with distance from start point to first intermediate point
        TransportPoint previousPoint = startPoint;
        double totalDistance = 0.0;
        
        // Add distances between consecutive intermediate points
        for (TransportPoint currentPoint : gpsPoints) {
            // Skip invalid points (null or zero coordinates)
            if (isInvalidCoordinate(currentPoint.getLatitude(), currentPoint.getLongitude())) {
                log.debug("Skipping invalid GPS point with null or zero coordinates");
                continue;
            }
            
            double segmentDistance = calculateHaversineDistance(
                previousPoint.getLatitude(), previousPoint.getLongitude(),
                currentPoint.getLatitude(), currentPoint.getLongitude());
            
            // Only add valid distances (ignore negative or NaN values)
            if (segmentDistance >= 0 && !Double.isNaN(segmentDistance)) {
                totalDistance += segmentDistance;
                log.debug("Added segment distance: {}m (total: {}m)", segmentDistance, totalDistance);
                previousPoint = currentPoint;
            } else {
                log.debug("Skipping invalid segment distance: {}", segmentDistance);
            }
        }
        
        // Add distance from last intermediate point to end point
        double finalSegmentDistance = calculateHaversineDistance(
            previousPoint.getLatitude(), previousPoint.getLongitude(),
            endPoint.getLatitude(), endPoint.getLongitude());
        
        if (finalSegmentDistance >= 0 && !Double.isNaN(finalSegmentDistance)) {
            totalDistance += finalSegmentDistance;
            log.debug("Added final segment distance: {}m (total: {}m)", finalSegmentDistance, totalDistance);
        }
        
        log.debug("Calculated precise transport distance: {}m", totalDistance);
        return totalDistance;
    }
    
    /**
     * Check if coordinates are invalid (null or zero)
     * 
     * @param latitude  the latitude value
     * @param longitude the longitude value
     * @return true if coordinates are invalid, false otherwise
     */
    private boolean isInvalidCoordinate(BigDecimal latitude, BigDecimal longitude) {
        return latitude == null || longitude == null || 
               BigDecimal.ZERO.compareTo(latitude) == 0 || BigDecimal.ZERO.compareTo(longitude) == 0;
    }
    
    /**
     * Calculate distance between two points using Haversine formula
     * 
     * @param lat1 Latitude of point 1
     * @param lon1 Longitude of point 1
     * @param lat2 Latitude of point 2
     * @param lon2 Longitude of point 2
     * @return Distance in meters
     */
    private double calculateHaversineDistance(java.math.BigDecimal lat1, java.math.BigDecimal lon1, java.math.BigDecimal lat2, java.math.BigDecimal lon2) {
        // Check for null values
        if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) {
            return 0.0;
        }
        
        // Check for zero coordinates
        if (java.math.BigDecimal.ZERO.compareTo(lat1) == 0 || java.math.BigDecimal.ZERO.compareTo(lon1) == 0 ||
            java.math.BigDecimal.ZERO.compareTo(lat2) == 0 || java.math.BigDecimal.ZERO.compareTo(lon2) == 0) {
            return 0.0;
        }
        
        // Convert BigDecimal to double for calculation
        double lat1Double = lat1.doubleValue();
        double lon1Double = lon1.doubleValue();
        double lat2Double = lat2.doubleValue();
        double lon2Double = lon2.doubleValue();
        
        final int EARTH_RADIUS = 6371; // Earth radius in kilometers
        
        // Convert latitude and longitude from degrees to radians
        double lat1Rad = Math.toRadians(lat1Double);
        double lon1Rad = Math.toRadians(lon1Double);
        double lat2Rad = Math.toRadians(lat2Double);
        double lon2Rad = Math.toRadians(lon2Double);
        
        // Haversine formula
        double deltaLat = lat2Rad - lat1Rad;
        double deltaLon = lon2Rad - lon1Rad;
        double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
                   Math.cos(lat1Rad) * Math.cos(lat2Rad) *
                   Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        
        // Distance in meters
        return EARTH_RADIUS * c * 1000;
    }
    
    /**
     * Count vibration records in the time window
     */
    public int countVibrationRecords() {
        return vibrationRecords.size();
    }
    
    /**
     * Check if radar condition is met (radar distance >= minRadarDistance for tRadarTime seconds)
     * @param minRadarDistance The minimum radar distance required
     * @param radarTimeWindow The time window in seconds that the condition must be maintained
     * @return true if the radar condition is met for the required duration, false otherwise
     */
    public boolean isRadarConditionMet(double minRadarDistance, int radarTimeWindow) {
        log.debug("Starting radar condition check: minRadarDistance={}, radarTimeWindow={}s", minRadarDistance, radarTimeWindow);
        
        // Special case: radar distance of 0 means infinity - always valid
        if (minRadarDistance == 0) {
            log.debug("Radar distance is 0 (infinity), condition always met");
            return true;
        }
        
        // Check if we have sustained radar distance >= minRadarDistance for the required duration
        if (radarRecords.isEmpty()) {
            log.debug("No radar records available for condition check");
            return false;
        }
        
        // Convert the queue to a list for easier processing
        java.util.List<RadarRecord> records = new java.util.ArrayList<>(radarRecords);
        
        // Sort records by timestamp
        records.sort((r1, r2) -> r1.getTimestamp().compareTo(r2.getTimestamp()));
        
        log.debug("Processing {} radar records for condition check", records.size());
        for (int i = 0; i < records.size(); i++) {
            RadarRecord record = records.get(i);
            log.debug("Record {}: timestamp={}, radarDistance={}", i, record.getTimestamp(), record.getRadarDistance());
        }
        
        // We need to check if the radar distance has been continuously >= minRadarDistance 
        // for at least radarTimeWindow seconds (using the device configured time, not adding 4 seconds)
        long requiredDurationMs = radarTimeWindow * 1000L;
        log.debug("Required duration: {}ms (device configured time)", requiredDurationMs);
        
        // Find the longest sequence of consecutive records from the end that meet the condition
        // IMPORTANT: Any record with radarDistance == 65535 (no signal) invalidates the entire sequence
        int consecutiveCount = 0;
        
        // Work backwards to find consecutive records that meet the condition
        // If we encounter any record with radarDistance == 65535, we must invalidate the entire sequence
        boolean hasNoSignal = false;
        
        for (int i = records.size() - 1; i >= 0; i--) {
            RadarRecord record = records.get(i);
            log.debug("Checking record {}: timestamp={}, radarDistance={}", i, record.getTimestamp(), record.getRadarDistance());
            
            // Check if this record meets the distance condition
            // Special handling for radar distance values:
            // 0 = infinity (always valid)
            // 65535 = no signal (never valid)
            if (record.getRadarDistance() != null) {
                if (record.getRadarDistance() == 65535) {
                    // 65535 means no signal - never valid, mark that we have no signal and break
                    hasNoSignal = true;
                    break;
                } else if (record.getRadarDistance() == 0 || record.getRadarDistance() >= minRadarDistance) {
                    // Either 0 (infinity) or >= minRadarDistance, condition met
                    consecutiveCount++;
                } else {
                    // Condition not met, break
                    break;
                }
            } else {
                // Null radar distance, break
                break;
            }
        }
        
        // If we encountered a no signal (65535) value, the entire sequence is invalid
        if (hasNoSignal) {
            log.debug("Radar condition not met: encountered no signal (65535) value");
            return false;
        }
        
        // If no records meet the condition, return false
        if (consecutiveCount == 0) {
            log.debug("No records meet the minimum distance requirement: {}", minRadarDistance);
            return false;
        }
        
        log.debug("Consecutive records meeting condition from the end: {}", consecutiveCount);
        
        // For a more accurate duration calculation, we need to consider the actual time span
        // between the first and last records in the consecutive sequence
        if (consecutiveCount > 0) {
            // Get the first and last records in the consecutive sequence
            int lastIndex = records.size() - 1;
            int firstIndex = records.size() - consecutiveCount;
            
            RadarRecord lastRecord = records.get(lastIndex);
            RadarRecord firstRecord = records.get(firstIndex);
            
            long actualDuration = lastRecord.getTimestamp().getTime() - firstRecord.getTimestamp().getTime();
            log.debug("Actual time span of consecutive records: {}ms (from {} to {})", 
                    actualDuration, firstRecord.getTimestamp(), lastRecord.getTimestamp());
            
            // Add a small buffer to account for the fact that each record represents a moment in time,
            // not a duration. We'll add half the estimated interval between records.
            long buffer = 0;
            if (records.size() >= 2) {
                RadarRecord prevRecord = records.get(records.size() - 2);
                long interval = lastRecord.getTimestamp().getTime() - prevRecord.getTimestamp().getTime();
                if (interval > 0) {
                    buffer = interval / 2;
                    log.debug("Adding buffer of {}ms based on interval of {}ms", buffer, interval);
                }
            }
            
            long adjustedDuration = actualDuration + buffer;
            log.debug("Adjusted duration with buffer: {}ms", adjustedDuration);
            
            boolean conditionMet = adjustedDuration >= requiredDurationMs;
            log.debug("Radar condition check result: adjusted duration={}ms >= required={}ms = {}", 
                    adjustedDuration, requiredDurationMs, conditionMet);
            
            return conditionMet;
        }
        
        log.debug("No valid duration calculation possible");
        return false;
    }
    
    /**
     * Clean up old records in sliding windows
     */
    public void cleanupOldRecords(int vibrationTimeWindow, int radarTimeWindow) {
        Date now = new Date();
        
        log.debug("Starting cleanup of old records: vibrationTimeWindow={}s, radarTimeWindow={}s", 
                vibrationTimeWindow, radarTimeWindow);
        log.debug("Current time: {}", now);
        
        // Clean up vibration records
        int vibrationCountBefore = vibrationRecords.size();
        while (!vibrationRecords.isEmpty() && 
               (now.getTime() - vibrationRecords.peek().getTimestamp().getTime()) > vibrationTimeWindow * 1000) {
            vibrationRecords.poll();
        }
        int vibrationCountAfter = vibrationRecords.size();
        log.debug("Cleaned up {} vibration records ({} -> {})", 
                vibrationCountBefore - vibrationCountAfter, vibrationCountBefore, vibrationCountAfter);
        
        // Clean up radar records
        // Modified to use device's t_radar_time + 6 seconds as the cleanup time window
        int adjustedRadarTimeWindow = radarTimeWindow + 6;
        int radarCountBefore = radarRecords.size();
        while (!radarRecords.isEmpty() && 
               (now.getTime() - radarRecords.peek().getTimestamp().getTime()) > adjustedRadarTimeWindow * 1000) {
            radarRecords.poll();
        }
        int radarCountAfter = radarRecords.size();
        log.debug("Cleaned up {} radar records ({} -> {})", 
                radarCountBefore - radarCountAfter, radarCountBefore, radarCountAfter);
    }
}